extends layout

block headContent
	title Mempool Summary

block content
	+pageTitle("Mempool Summary")

	
	+dismissableInfoAlert("mempoolSummaryNoteDismissed", "About Mempool Summary...")
		.mb-2 This tool summarizes data about the transactions in your node's <b>mempool</b> - the set of transactions that your node has received from the network but are not yet confirmed in a block.
		.mb-0 The fee-rate summary data can be helpful for deciding what fee is necessary to get a transaction included in a future block, depending on its priority.


	div#progress-wrapper.mb-huge
		.card.shadow-sm.mb-3
			.card-body
				.d-flex.align-items-center
					.spinner-border.spinner-border-sm.text-primary.me-2.ms-2
					span.fw-bold.me-2 Loading:
					span.fw-light(id="progress-text") ...
				
				div.progress.mt-2(id="progress-bar", style="height: 7px;")
					div.progress-bar(id="data-progress", role="progressbar", aria-valuenow="0", aria-valuemin="0" ,aria-valuemax="100")

				div.loading-error
	

	div(id="main-content", style="display: none;")
		+contentSection("Summary")
			+summaryRow(5)
				+summaryItem("Transactions")
					span#tx-count

				+summaryItem("Blocks", "The number of blocks filled by all current mempool transactions.")
					span#block-count

				+summaryItem("Total Fees")
					span#total-fees

				+summaryItem("Avg Fee")
					span#avg-fee

				+summaryItem("Avg Fee Rate", null, "sat/vB")
					span#avg-fee-rate

		+contentSection("Estimate Block Depth")
			p If you already have a transaction in the mempool, you can enter its <b>TXID</b> to get an estimate for when it will be included in a block. Or, if you're planning to send a transaction, you can estimate confirmation time for different fee rates.

			.clearfix
				.float-start
					.me-4.d-inline-block.mb-3
						h6.text-card-highlight.fw-light
							span.text-uppercase Fee Rate
							small.ms-1 (sat/vB)

						- var feeRatesToEstimate = [1, 2, 3, 4, 5, 15, 25, 50, 75, 100, 150];

						div.btn-group
							each feeRateToEstimate in feeRatesToEstimate
								a.btn.btn-outline-primary.fee-rate-btn(href="javascript:void(0)", onclick=`estimateMempoolDepth(parseInt($(this).attr("data-fee-rate"))); return false;`, data-fee-rate=feeRateToEstimate) #{feeRateToEstimate}

					.me-4.d-inline-block.mb-3
						h6.text-card-highlight.text-uppercase.fw-light Transaction ID

						form.form-inline(method="post", action="javascript:void(0)", onsubmit=`estimateTransactionMempoolDepth($("#mempoolquery").val()); return false;` style="width: 325px;")
							.input-group.input-group
								input.form-control.form-control(id="mempoolquery", type="text", name="mempoolquery", placeholder="txid", value=(mempoolquery), title="<span class='text-warning'>Transaction not found</span>", data-bs-trigger="manual", data-bs-html="true", data-bs-placement="bottom")
								
								button.btn.btn-primary(type="submit", aria-label="Search")
									i.bi-search

					.me-4.d-inline-block.mb-3
						h6.text-card-highlight.fw-light
							span.text-uppercase Custom Fee Rate
							small.ms-1 (sat/vB)

						form.form-inline(method="post", action="javascript:void(0)", onsubmit=`estimateMempoolDepth($("#customFeeRate").val()); return false;` style="width: 200px;")
							.input-group.input-group
								input.form-control.form-control(id="customFeeRate", type="text", name="customFeeRate", placeholder="sat/vB", value=(customFeeRate))
								
								button.btn.btn-primary(type="submit", aria-label="Search")
									i.bi-search

			hr.mt-1.mb-3

			+summaryRow(3)
				+summaryItem("Fee Rate", null, "sat/vB")
					span#estimate-fee-rate -

				+summaryItem("Est. Block Depth")
					span#estimate-mempooldepth -

				+summaryItem("Est. Confirmation Time")
					span#estimate-conf-time -



		div#detail-charts-and-data
			+contentSection("Fee Rate Distribution")
				canvas.mb-3(id="mempoolBarChart", height="100")

				hr.mt-3

				.table-responsive
					table.table.table-striped.table-borderless.mb-3
						thead
							tr
								th.text-card-highlight.text-uppercase.fw-light Fee Rate
								th.text-end.text-card-highlight.text-uppercase.fw-light N(tx)
								th.text-end.text-card-highlight.text-uppercase.fw-light
									span.border-dotted(title="The number of blocks filled by all transactions in this fee-rate bucket.", data-bs-toggle="tooltip") N(blocks)

								th.text-end.text-card-highlight.text-uppercase.fw-light
									span.border-dotted(title="The running total of the number of blocks filled by transactions at this fee-rate and higher.", data-bs-toggle="tooltip") Σ N(blocks)
								th.text-end.text-card-highlight.text-uppercase.fw-light Σ Fees
								th.text-end.text-card-highlight.text-uppercase.fw-light Avg Fee
								th.text-end.text-card-highlight.fw-light
									span.text-uppercase Avg Fee Rate
									small.ms-1 (sat/vB)
						tbody(id="fee-rate-table-body")
							tr(id="fee-rate-table-row-prototype", style="display: none;")
								td.data-label
								td.text-end.data-count
								td.text-end.data-block-count
								td.text-end.data-sum-block-count
								td.text-end.data-total-fees
								td.text-end.data-avg-fee
								td.text-end.data-fee-rate

							tr(id="empty-row-to-fix-striped-coloring", style="display: none;")

			

			+contentSection("Size Distribution")
				canvas.mb-3(id="txSizesBarChart", height="100")

			

			+contentSection("Age Distribution")
				canvas.mb-3(id="txAgesBarChart", height="100")

			.row
				.col
					+contentSection("Highest Fee Rate Transactions")
						.table-responsive
							table.table.table-striped.table-borderless.mb-3
								thead
									tr
										th.text-card-highlight.text-uppercase.fw-light Txid
										th.text-end.text-card-highlight.fw-light
											span.text-uppercase Fee Rate
											span.ms-2 (sat/vB)

								tbody(id="topfee-tx-table-body")
									tr(id="topfee-tx-table-row-prototype", style="display: none;")
										td.data-txid
										td.text-end.data-feerate

									tr(id="empty-row-to-fix-striped-coloring", style="display: none;")

				.col
					+contentSection("Oldest Transactions")
						.table-responsive
							table.table.table-striped.table-borderless.mb-3
								thead
									tr
										th.text-card-highlight.text-uppercase.fw-light Txid
										th.text-end.text-card-highlight.text-uppercase.fw-light
											span.border-dotted(title="How long ago this transaction was first seen by your node.", data-bs-toggle="tooltip")
												| Age

								tbody(id="oldest-tx-table-body")
									tr(id="oldest-tx-table-row-prototype", style="display: none;")
										td.data-txid
										td.text-end.data-age

									tr(id="empty-row-to-fix-striped-coloring", style="display: none;")

				.col
					+contentSection("Largest Transactions")
						.table-responsive
							table.table.table-striped.table-borderless.mb-3
								thead
									tr
										th.text-card-highlight.text-uppercase.fw-light Txid
										th.text-end.text-card-highlight.text-uppercase.fw-light Size

								tbody(id="largest-tx-table-body")
									tr(id="largest-tx-table-row-prototype", style="display: none;")
										td.data-txid
										td.text-end.data-size

									tr(id="empty-row-to-fix-striped-coloring", style="display: none;")


			if (false)
				pre
					code.json#json-content
		

block endOfBody

	+graphPageScriptSetup
	

	script.
		var satoshiPerByteBucketMaxima = !{JSON.stringify(satoshiPerByteBucketMaxima)};
		var statusId = Math.random().toString(36).substr(2, 5);
		var statusInterval;
		var summary = null;

		$(document).ready(function() {
			statusInterval = setInterval(function() { updateStatus(); }, 125);

			loadMempool();
		});

		function updateStatus() {
			$.ajax({
				url: `./internal-api/mempool-summary-status?statusId=${statusId}`

			}).done((res) => {
				if (!res.count) {
					// not started yet
					return;
				}

				var percent = new Decimal(res.done).dividedBy(res.count).times(100);

				$("#data-progress").css("width", `${percent.toDP(0)}%`);
				$("#progress-text").html(`<span class='fw-bold d-inline-block' style="width: 55px;">${percent.toDP(1)}%</span><span class='small fw-light ms-3'>(${res.done.toLocaleString()} of ${res.count.toLocaleString()})</span>`);

				if (res.done == res.count) {
					if (summary == null) {
						$.ajax({
							url: `./internal-api/get-mempool-summary?statusId=${statusId}`

						}).done((summaryResult) => {
							if (summaryResult && summaryResult.count) {
								summary = summaryResult;
							
								$("#json-content").text(JSON.stringify(summaryResult, null, 4));

								hljs.highlightAll();

								displaySummaryData(summaryResult);

								clearInterval(statusInterval);
							}
						}).always(() => {
							// nothing
						});
					}
				}
			}).fail((a, b, c) => {
			}).always(() => {
			});
		}

		function loadMempool() {
			$.ajax({
				url: `./internal-api/build-mempool-summary?statusId=${statusId}`

			}).done((buildResult) => {
				// we update status elsewhere, this call just kicks off the build

			}).fail((jqXHR, textStatus, errorThrown) => {
				$(".loading-error").html(`<h6 class='mt-4 text-danger'>Failed loading mempool:</h6><pre><code class='json'>${JSON.stringify(textStatus)}</code></pre><pre><code class='json'>${JSON.stringify(errorThrown)}</code></pre>`);

				hljs.highlightAll();

			}).always(() => {
			});
		}

		function displaySummaryData(summary) {
			var feeRateGraphData = buildFeeRateGraphData(summary);
			var txSizeGraphData = buildTxSizeGraphData(summary);
			var txAgeGraphData = buildTxAgeGraphData(summary);

			fillTopfeeTxTable(summary);
			fillOldestTxTable(summary);
			fillLargestTxTable(summary);

			//console.log(JSON.stringify(summary));

			$("#tx-count").text(summary.count.toLocaleString());
			$("#block-count").text(new Decimal(summary.totalWeight).dividedBy(4000000).toDP(1)); // TODO: magic number: block weight
			$("#total-fees").text(summary.totalFees);
			
			if (summary.count == 0) {
				$("#avg-fee").text("-");
				$("#avg-fee-rate").text("-");

				$("#detail-charts-and-data").hide();

				$("#main-content").show();
				$("#progress-wrapper").hide();

				return;
			}
			
			$("#avg-fee").text(summary.averageFee);
			$("#avg-fee-rate").text(summary.averageFeePerByte);

			$.ajax({
				url: `./internal-api/utils/formatLargeNumber/${summary.totalBytes},1`

			}).done(function(result) {
				$("#mem-usage").html(`<span>${result[0]} <small>${result[1].abbreviation}B</small></span>`);
			});

			updateCurrencyValue($("#total-fees"), summary.totalFees);
			updateCurrencyValue($("#avg-fee"), summary.averageFee);

			updateFeeRateValue($("#avg-fee-rate"), summary.averageFeePerByte, 2, false);


			//$("#summary-json").text(JSON.stringify(summary, null, 4));


			// fee rate chart
			var ctx1 = document.getElementById("mempoolBarChart").getContext('2d');
			var mempoolBarChart = new Chart(ctx1, {
				type: 'bar',
				data: {
					labels: feeRateGraphData.feeBucketLabels.reverse(),
					datasets: [
						/*{
							data: feeRateGraphData.lineData,
							type: "line",
							yAxisID: "axis-block-count",
							borderColor: "white",
							lineTension: 0,
							borderWidth: 2,
							pointRadius: 2
						},*/
						{
							data: feeRateGraphData.blockCounts.reverse(),
							backgroundColor: feeRateGraphData.bgColors,
							//yAxisID: "axis-tx-count",
						},
					]
				},
				options: {
					interaction: {
						intersect: false,
						mode: 'index',
					},
					plugins: {
						legend: {
							display: false
						},
					},
					scales: {
						x: {
							grid: {
								color: gridLineColor
							},
							title: {
								display: true,
								text: "Fee Rate (sat/vB)"
							},
						},
						y: {
							id: "axis-tx-count",
							title: {
								display: true,
								text: "Block Count"
							},
							type: 'logarithmic',
							ticks: {
								beginAtZero:true,
							},
							grid: {
								color: gridLineColor
							}
						},
						/*{
							id: "axis-block-count",
							position: "left",
							ticks: {
								beginAtZero:true,
							},
							grid: {
								color: gridLineColor
							}
						}*/
					}
				}
			});

			// tx size chart
			var ctx2 = document.getElementById("txSizesBarChart").getContext('2d');
			var txSizesBarChart = new Chart(ctx2, {
				type: 'bar',
				data: {
					labels: txSizeGraphData.sizeBucketLabels,
					datasets: [{
						data: txSizeGraphData.sizeBucketTxCounts,
						backgroundColor: txSizeGraphData.bgColors
					}]
				},
				options: {
					interaction: {
						intersect: false,
						mode: 'index',
					},
					plugins: {
						legend: {
							display: false
						},
					},
					scales: {
						x: {
							title: {
								display: true,
								text: "Transaction Size (bytes)"
							},
							grid: {
								color: gridLineColor
							}
						},
						y: {
							title: {
								display: true,
								text: "Transaction Count"
							},
							type: "logarithmic",
							ticks: {
								beginAtZero:true
							},
							grid: {
								color: gridLineColor
							}
						}
					}
				}
			});

			// tx age chart
			var ctx3 = document.getElementById("txAgesBarChart").getContext('2d');
			var txSizesBarChart = new Chart(ctx3, {
				type: 'bar',
				data: {
					labels: txAgeGraphData.ageBucketLabels,
					datasets: [{
						data: txAgeGraphData.ageBucketTxCounts,
						backgroundColor: txAgeGraphData.bgColors
					}]
				},
				options: {
					interaction: {
						intersect: false,
						mode: 'index',
					},
					plugins: {
						legend: {
							display: false
						},
					},
					scales: {
						x: {
							title: {
								display: true,
								text: "Transaction Age"
							},
							grid: {
								color: gridLineColor
							}
						},
						y: {
							title: {
								display: true,
								text: "Transaction Count"
							},
							type: "logarithmic",
							ticks: {
								beginAtZero:true
							},
							grid: {
								color: gridLineColor
							}
						}
					}
				}
			});


			// fee rate table
			var sumBlockCount = new Decimal(0);

			for (var i = (summary.satoshiPerByteBuckets.length - 1); i >= 0; i--) {
				var item = summary.satoshiPerByteBuckets[i];

				var row = $("#fee-rate-table-row-prototype").clone();
				row.attr("id", null);
				row.addClass("fee-rate-table-row");

				var blockCount = new Decimal(item.totalWeight).dividedBy(4000000); // TODO: magic number: block weight
				var sumBlockCount = sumBlockCount.plus(blockCount);

				row.find(".data-label").text(summary.satoshiPerByteBucketLabels[i]);
				row.find(".data-count").text(item.count.toLocaleString());
				row.find(".data-block-count").text(blockCount.toDP(2));
				row.find(".data-sum-block-count").text(sumBlockCount.toDP(2));
				row.find(".data-total-fees").text(item.count > 0 ? item.totalFees : "-");
				row.find(".data-avg-fee").text(item.count > 0 ? item.totalFees / item.count : "-");
				row.find(".data-fee-rate").text("-");

				if (item.count > 0) {
					updateCurrencyValue(row.find(".data-total-fees"), item.totalFees);
					updateCurrencyValue(row.find(".data-avg-fee"), item.totalFees / item.count);

					updateFeeRateValue(row.find(".data-fee-rate"), item.totalFees / item.totalBytes, 2, false);
				}

				row.show();

				$("#fee-rate-table-body").append(row);
			}
			

			$("#main-content").show();
			$("#progress-wrapper").hide();
		}

		function fillTopfeeTxTable(summary) {
			for (var i = 0; i < summary.highestFeeTxs.length; i++) {
				var topfeeTx = summary.highestFeeTxs[i];

				var row = $("#topfee-tx-table-row-prototype").clone();
				row.attr("id", null);
				row.addClass("topfee-tx-table-row");

				row.find(".data-txid").html(`<a href="./tx/${topfeeTx.txid}">${topfeeTx.txid.substring(0, 16)}...</a>`);

				row.find(".data-feerate").text(new Decimal(topfeeTx.feePerByte).times(4).toDP(1));

				row.show();

				$("#topfee-tx-table-body").append(row);
			}
		}

		function fillOldestTxTable(summary) {
			for (var i = 0; i < summary.oldestTxs.length; i++) {
				var oldTx = summary.oldestTxs[i];

				var row = $("#oldest-tx-table-row-prototype").clone();
				row.attr("id", null);
				row.addClass("oldest-tx-table-row");

				row.find(".data-txid").html(`<a href="./tx/${oldTx.txid}">${oldTx.txid.substring(0, 16)}...</a>`);

				var days = new Decimal(oldTx.age).dividedBy(1).dividedBy(60 * 60 * 24);

				if (days > 30) {
					var months = days.dividedBy(30);

					row.find(".data-age").text(months.toDP(2) + " mo");

				} else {
					row.find(".data-age").text(days.toDP(2) + " d");
				}

				row.show();

				$("#oldest-tx-table-body").append(row);
			}
		}

		function fillLargestTxTable(summary) {
			for (var i = 0; i < summary.largestTxs.length; i++) {
				var largeTx = summary.largestTxs[i];

				var row = $("#largest-tx-table-row-prototype").clone();
				row.attr("id", null);
				row.addClass("largest-tx-table-row");

				row.find(".data-txid").html(`<a href="./tx/${largeTx.txid}">${largeTx.txid.substring(0, 16)}...</a>`);

				var kb = new Decimal(largeTx.size).dividedBy(1000);

				row.find(".data-size").text(kb.toDP(2) + " kvB");

				row.show();

				$("#largest-tx-table-body").append(row);
			}
		}

		function buildFeeRateGraphData(summary) {
			var feeBucketLabels = [];
			
			var blockCounts = [];
			for (var i = 0; i < summary["satoshiPerByteBuckets"].length; i++) {
				var item = summary["satoshiPerByteBuckets"][i];

				if (i < summary["satoshiPerByteBuckets"].length - 1) {
					feeBucketLabels.push(item.minFeeRate);
					blockCounts.push(item.totalWeight / 4000000);
				}
			}

			feeBucketLabels.shift();

			var lineData = [];
			var totalWeight = 0;
			for (var i = summary.satoshiPerByteBuckets.length - 1; i >= 0; i--) {
				lineData.push(summary.satoshiPerByteBuckets[i].totalWeight / 4000000 + totalWeight);
				totalWeight += summary.satoshiPerByteBuckets[i].totalWeight / 4000000;
			}

			feeBucketLabels.push((summary.satoshiPerByteBucketMaxima[summary.satoshiPerByteBucketMaxima.length - 1] + "+"));
				
			var totalfeeBuckets = summary["satoshiPerByteBucketTotalFees"];
			
			var graphData = {
				feeBucketLabels:feeBucketLabels.map(x => [x]),
				bgColors:[],
				lineData:lineData,
				blockCounts:blockCounts
			};

			for (var i = 0; i < feeBucketLabels.length; i++) {
				var feeBucketLabel = feeBucketLabels[i];
				//var percentTx = Math.round(100 * feeBucketTxCounts[i] / summary.count).toLocaleString();
				
				//graphData.feeBucketLabels.push([feeBucketLabel]);//, `${feeBucketTxCounts[i]} tx (${percentTx}%)`]);
				graphData.bgColors.push(`hsl(${(333 * i / feeBucketLabels.length)}, 100%, 50%)`);
			}

			return graphData;
		}

		function buildTxSizeGraphData(summary) {
			var sizeBucketLabels = [];
			var bgColors = [];

			for (var i = 0; i < summary.sizeBucketLabels.length; i++) {
				var sizeBucketLabel = summary.sizeBucketLabels[i];
				var percentTx = Math.round(100 * summary.sizeBucketTxCounts[i] / summary.count).toLocaleString();

				sizeBucketLabels.push([`${sizeBucketLabel}`]);
				bgColors.push(`hsl(${(333 * i / summary.sizeBucketLabels.length)}, 100%, 50%)`);
			}

			return {
				sizeBucketLabels: sizeBucketLabels,
				bgColors: bgColors,
				sizeBucketTxCounts: summary.sizeBucketTxCounts
			};
		}

		function buildTxAgeGraphData(summary) {
			var ageBucketLabels = [];
			var bgColors = [];

			for (var i = 0; i < summary.ageBucketLabels.length; i++) {
				var ageBucketLabel = summary.ageBucketLabels[i];
				var percentTx = Math.round(100 * summary.ageBucketTxCounts[i] / summary.count).toLocaleString();

				ageBucketLabels.push([`${ageBucketLabel}`]);
				bgColors.push(`hsl(${(333 * i / summary.ageBucketLabels.length)}, 100%, 50%)`);
			}

			return {
				ageBucketLabels: ageBucketLabels,
				bgColors: bgColors,
				ageBucketTxCounts: summary.ageBucketTxCounts
			};
		}

		var notfoundEl = document.getElementById("mempoolquery");
		var notfoundTooltip = new bootstrap.Tooltip(notfoundEl);

		function estimateTransactionMempoolDepth(txid) {
			var avgTransactionsPerBlock = 3000;
			var satsPerBitcoin = 100000000; // TODO: magic number - replace with coinConfig.baseCurrencyUnit.multiplier
			
			$.ajax({
				url: `./internal-api/mempool-tx-summaries/${txid}`

			}).done(function(resultList) {
				if (resultList && resultList.length > 0) {
					var result = resultList[0];

					var feeRate = new Decimal(result.f).dividedBy(result.sz); // TODO: magic number, sat/BTC

					estimateMempoolDepth(feeRate);

				} else {
					notfoundTooltip.show();
					setTimeout(() => { notfoundTooltip.hide(); }, 2000);
				}
			});
		}

		function estimateMempoolDepth(feeRate) {
			feeRate = new Decimal(feeRate);
			
			if (feeRate > 1000) {
				$("#estimate-fee-rate").text(parseInt(feeRate).toLocaleString());

			} else {
				$("#estimate-fee-rate").text(new Decimal(feeRate).toDP(1));
			}

			var sumBlockCount = new Decimal(0);

			for (var i = (summary.satoshiPerByteBuckets.length - 1); i >= 0; i--) {
				var item = summary.satoshiPerByteBuckets[i];

				var blockCount = new Decimal(item.totalWeight).dividedBy(4000000); // TODO: magic number: block weight
				
				if (feeRate >= (item.minFeeRate || 1) && feeRate < (item.maxFeeRate || 10000000)) {
					// in this bucket
					var minBlocks = sumBlockCount;
					var maxBlocks = sumBlockCount.plus(blockCount);

					if (minBlocks < 1) {
						minBlocks = new Decimal(1);
					}

					if (maxBlocks < 1) {
						maxBlocks = new Decimal(1);
					}

					if (`${minBlocks.toDP(1)}` == `${maxBlocks.toDP(1)}`) {
						$("#estimate-mempooldepth").text(`~${minBlocks.toDP(1)}`);

					} else {
						$("#estimate-mempooldepth").text(`${Math.floor(minBlocks)} - ${Math.ceil(maxBlocks)}`);
					}
					
					var minMinutes = minBlocks.times(10); // TODO: magic number: 10min
					var maxMinutes = maxBlocks.times(10); // TODO: magic number: 10 min

					if (maxMinutes < 60) {
						if (`${minMinutes.toDP(1)}` == `${maxMinutes.toDP(1)}`) {
							$("#estimate-conf-time").text(`~${minMinutes.toDP(0)} min`);

						} else {
							$("#estimate-conf-time").text(`${minMinutes.toDP(0)} - ${maxMinutes.toDP(0)} min`);
						}
					} else if (maxMinutes < 60 * 24) {
						$("#estimate-conf-time").text(`${minMinutes.dividedBy(60).toDP(1)} - ${maxMinutes.dividedBy(60).toDP(1)} hr`);

					} else if (minMinutes < 60 * 24) {
						$("#estimate-conf-time").text(`${minMinutes.dividedBy(60).toDP(1)} hr - ${maxMinutes.dividedBy(60 * 24).toDP(1)} day`);

					} else {
						$("#estimate-conf-time").text(`${minMinutes.dividedBy(60 * 24).toDP(1)} - ${maxMinutes.dividedBy(60 * 24).toDP(1)} day`);
					}
				}

				sumBlockCount = sumBlockCount.plus(blockCount);
			}
		}
